/* (c) Copyright 2009 Data Constellation and Clifford Heath. ALL RIGHTS RESERVED */
/*jslint devel: true, laxbreak: true, forin: true */
function OrmDiagram(paper) {
  var Key = {
    BACKSPACE: 8,
    TAB: 9,
    ENTER: 13,
    SHIFT: 16,
    CONTROL: 17,
    CAPS_LOCK: 20,
    ESCAPE: 27,
    SPACE: 32,
    PAGE_UP: 33,
    PAGE_DOWN: 34,
    END: 35,
    HOME: 36,
    LEFT: 37,
    UP: 38,
    RIGHT: 39,
    DOWN: 40,
    INSERT: 45,
    DELETE: 46,
    NUMPAD_MULTIPLY: 106,
    NUMPAD_ADD: 107,
    NUMPAD_ENTER: 108,
    NUMPAD_SUBTRACT: 109,
    NUMPAD_DECIMAL: 110,
    NUMPAD_DIVIDE: 111,
    COMMA: 188,
    PERIOD: 190
  };

  var log = function(a) {
    if (typeof console != 'undefined')
      console.log(a);
    else if ($.browser.opera)
      opera.postError(a);
    else
      alert(a);
  };

  var background = paper
    .rect(0, 0, 5000, 5000)	// Opera can't hande 10000, 10000!!
    .attr({fill: "#000", "fill-opacity": 0, "stroke-width": 0});

  if (typeof paper.showHelp == 'undefined') {
    paper.showHelp = function(str) {}; // Stub
  }

  paper.pageOffset = function() {
    return $.browser.opera ? $(this.canvas.parentNode).offset() : $(this.canvas).offset();
  };

  var array_delete = function(a, e) { // Delete any instances of e in a
    for (var i = 0; i < a.length; i++)
      if (a[i] == e)
	a.splice(i--, 1);
  };

  /*
   * Manage the currently selected objects
   */
  var selected_objects = [];
  paper.setSelection = function(obj) {
    this.deselectAll();
    this.select(obj);
  };

  paper.isSelected = function(obj) {
    for (var i in selected_objects)
      if (selected_objects[i] == obj) return true;
    return false;
  };

  paper.select = function(obj) {
    // Should be able to filter the different kinds of things selected.
    if (!paper.isSelected(obj)) {       // Don't select anything more than once!
      selected_objects.push(obj);
      paper.showHelp("Drag to move objects, Delete to delete, click the background to deselect");
    }
  };

  paper.deselect = function(obj) {
    for (var i in selected_objects)
      if (selected_objects[i] == obj)
        selected_objects.splice(i, 1);
  };

  paper.deselectAll = function() {
    while (selected_objects.length > 0)
      selected_objects[0].deselect();
    paper.showHelp("Double-click to create an object type");
  };

  /*
   * Create a rubber band line from this point to the start point.
   * Useful as a drag_obj (create one in dragStart).
   * finish() is called when the drag ends and the rubber band is deleted.
   */
  paper.RubberBand = function(fx, fy, to_x, to_y, update, finish) {
    var from_x = fx, from_y = fy;
    var rubber_x = 0, rubber_y = 0;
    var rubber_band = paper.path("M0 0").attr({stroke: "#000", "stroke-width": 2});
    rubber_band.setPath = function() {
      var path = "M"+from_x+" "+from_y+"L"+rubber_x+" "+rubber_y;
      rubber_band.attr({path: path});
    };
    rubber_band.resetOrigin = function(x, y) {
      if (from_x == x && from_y == y) return;
      from_x = x;
      from_y = y;
      rubber_band.setPath();
    };
    rubber_band.x = function() { return rubber_x; };
    rubber_band.y = function() { return rubber_y; };
    rubber_band.dragUpdate = function(draggingOver, dx, dy, event) {
      rubber_x += dx; rubber_y += dy;
      rubber_band.setPath();
      if (update)
	update(draggingOver, dx, dy, event);
    };
    rubber_band.dragFinish = function(dropped_on, x, y, event) {
      if (dropped_on && finish)
	finish(dropped_on, x, y, event);
      this.dragCancel();
    };
    rubber_band.dragCancel = function() {
      rubber_band.remove();
    };
    rubber_band.dragUpdate(null, to_x, to_y); // Show initial motion
    return rubber_band;
  };

  /*
   * A connector line between a player and a role.
   * A player can be anything that responds to x() and y().
   * A role is anything that responds to closest() that returns {x:INT, y:INT}
   */
  paper.RoleConnector = function(_player, _role) {
    var player = _player;
    var role = _role;
    var connector = this.path("M0 0").attr({stroke: "#000", "stroke-width": 2});

    connector.player = player;
    connector.role = role;
    connector.rolePos = function(ex, ey) { return role.closest(ex, ey); };
    connector.redraw = function() {
      var player_x = player.x(), player_y = player.y();
      var closest;

      if (player.constraint) {
	if (player.constraint.rolePairSlave(this))
	  this.hide();
	else if (!(closest = player.constraint.rolePairConnection(this)))
	  this.show();	// After pairing changes where we were hidden
      }

      if (!closest)
	closest = connector.rolePos(player_x, player_y);
      this.fromX = player_x;
      this.fromY = player_y;
      this.toX = closest.x;
      this.toY = closest.y;
      var path_str = "M"+player_x+" "+player_y+"L"+closest.x+" "+closest.y;
      // log("connector path is "+path_str);
      this.attr({path: path_str});
    };

    connector.translate = function(dx, dy) {
      player.translate(dx, dy);
      role.translate(dx, dy);
      if (this.lock_highlight)
	this.highlight();
      // No need to call real translate, will always be redrawn
    };
    connector._real_remove = connector.remove;
    connector.remove = function() {
      role.disconnect(this);
      player.disconnect(this);
      connector._real_remove();
    };
    connector.changePlayer = function(new_player) {
      this.player.disconnect(this);
      this.player = player = new_player;
      this.player.connect(this);
      this.redraw();
    };

    /*--------------- Highlighting ---------------*/
    connector.highlight = function() {
      var lock = role.lock;
      var player_x = player.x(), player_y = player.y();
      var closest = this.rolePos(player_x, player_y);
      var dx = (player_x-closest.x)*1.0, dy = (player_y-closest.y)*1.0;
      var slope = player_x == closest.x ? dy : dy/dx;
      var steep = slope < -0.5 || slope > 0.5;
      var cx = (player_x+closest.x)/2, cy = (player_y+closest.y)/2;
      if (steep)
	cx += 12; // To right of centre point if steep
      else
	cy += 12; // Below centre point if not steep
      var l = (lock&1) ? ((lock&2) ? 'l' : 'v') : ((lock&2) ? 'h' : ' ');
      if (this.lock_highlight)
	this.lock_highlight.attr({text: l, x: cx, y: cy});
      else if (lock)
	this.lock_highlight = paper.text(cx, cy, l).attr({"font-size" : 10});
    };

    connector.unhighlight = function() {
      if (this.lock_highlight)
	this.lock_highlight.remove();
      this.lock_highlight = null;
    };

    player.connect(connector);
    role.connect(connector);
    connector.redraw();
    return connector;
  };
  paper.ConstraintConnector = function(_constraint, _role) {
    var c = this.RoleConnector(_constraint, _role);
    c.rolePos = function(ex, ey) { return c.role.closest(ex, ey, true); };
    c.role.redraw();
    c.attr({stroke: "#808", "stroke-width": 1, "stroke-dasharray" : "-"});
    c.toBack();
    background.toBack();
    _constraint.toFront();
    return c;
  };

  paper.SubtypeConnector = function(supertype, subtype, secondary_subtype) {
    var sc = this.RoleConnector(subtype, supertype);
    sc.attr({stroke: "#808", "stroke-width": 3, "stroke-dasharray" : secondary_subtype ? "-" : " "});
    sc.toBack();
    background.toBack();

    // A subset connector also acts as a role for constraining:
    sc.closest = function() {
      return { x: (this.fromX+this.toX)/2, y: (this.fromY+this.toY)/2 };
    };
    var connectors = [];
    sc.connect = function(connector) {
      // The first connection is always the role player
      connectors.push(connector);
    };
    sc.disconnect = function(connector) {
      array_delete(connectors, connector);
    };

    // Subset connectors have an arrowhead on the superset end
    sc.redraw_super = sc.redraw;
    sc.redraw = function() {
      this.redraw_super();
      if (!this.arrow)
	this.arrow = paper.path("M0 0").
	  attr({ fill: "#808", stroke: "#808", "stroke-width": 0 });
      var angle = Math.atan2(this.toY-this.fromY, this.toX-this.fromX);
      // Arrow head is a triangle that fills a square
      this.arrow.attr({path: "M2 0m-20 -10l0 20l20 -10m18 0l0 1"})
	.rotate(angle*180/3.14159, 0,0)
	.translate(this.toX-1, this.toY-1);
      for (var c in connectors) {
        connectors[c].redraw();
      }
    };
    sc.redraw();

    // Shift-drag from a subset connector creates or adds to a constraint
    sc.draggable();
    sc.arrow.draggable({drag_obj: sc});
    sc.arrow.dragStart = function(x, y, down, move) {
      return sc.dragStart(x, y, down, move);
    };
    sc.dragStart = function(x, y, down, move) {
      if (!down.shiftKey) return false;

      var rubberBand = paper.RubberBand((this.fromX+this.toX)/2, (this.fromY+this.toY)/2, x, y, null, null)
	.attr({stroke: "#808", "stroke-width": 1, "stroke-dasharray" : "."})
	.toFront();
      sc.toFront();
      rubberBand.dragFinish = function(dropped_on, x, y, event) {
	if (dropped_on == background.node) {
	  paper.Constraint(x, y, [sc]);
	  // MESSAGE: New constraint
	} else if (dropped_on.raphael.constraint) {
	  dropped_on.raphael.constraint.addRoleSequence([sc]);
	  // MESSAGE: Add role set to constraint
	}
	rubberBand.remove();
      };
      rubberBand.dragCancel = function() { this.remove(); };
      return rubberBand;
    };

    return sc;
  };

  /*
   * A role in a fact type
   */
  var RoleWidth = 22, RoleHeight = 16;
  paper.Role = function(fact_type, player, _x, _y) {
    var paper = this;
    var role = paper.rect(_x-RoleWidth/2, _y-RoleHeight/2, RoleWidth, RoleHeight);
    role.attr({stroke: "#000", "stroke-width": 2.2, fill: "#FFF"});
    role.toFront();
    role.fact_type = fact_type;  // For drop targetting
    role.player = player;
    role.role = role;

    var select_hl = paper.rect(_x-RoleWidth/2+3,_y-RoleHeight/2+3,RoleWidth-6,RoleHeight-6);
    select_hl.attr({stroke: "#888", "stroke-width": 1, "stroke-dasharray" : "."});
    select_hl.role = role;
    role.select_hl = select_hl;
    select_hl.hide();

    var connectors = [];
    role.connect = function(connector) {
      // The first connection is always the role player
      connectors.push(connector);
    };
    role.disconnect = function(connector) {
      array_delete(connectors, connector);
    };

    /*--------------- Geometry ---------------*/
    role.x = function() { var b = this.getBBox(); return b.x+b.width/2; };
    role.y = function() { var b = this.getBBox(); return b.y+b.height/2; };

    role.closest = function(x, y, leave_mandatory) {
      // Find the closest connection point to x, y
      var b = this.getBBox();
      var cx = b.x+b.width/2, cy = b.y+b.height/2;
      var dy = y > cy ? y-cy : cy - y;
      var dx = x > cx ? x-cx : cx - x;
      var roles = this.fact_type.roles;
      var conn;

      if (roles[0] == this && x < cx && dx >= dy) {  // First role
        conn = { x: b.x, y: cy};
      }
      else if (roles[roles.length-1] == this && x > cx && dx >= dy) {  // Last role
        conn = { x: b.x+b.width, y: cy};
      } else {
        // Otherwise join to the middle of the top or bottom:
        conn = { x: cx, y: b.y+(y > cy ? b.height : 0)};
      }
      if (!leave_mandatory && role.isMandatory) {
        //log("Move dot to "+conn.toSource());
        this.mandatoryDot.attr({cx: conn.x, cy: conn.y});
      }

      return conn;
    };

    /*--------------- Object selection ---------------*/
    role.isSelected = function(add) {
      return paper.isSelected(this);
    };
    role.select = function(add) {
      if (this.isSelected()) { return; }
      fact_type.deselect();
      if (!add) { paper.deselectAll(); }
      this.select_hl.show();
      this.select_hl.toFront();	// Necessary in Opera only
      paper.select(this);
    };
    role.deselect = function() {
      this.select_hl.hide();
      paper.deselect(this);
    };

    /*--------------- Mandatory constraint ---------------*/
    role.isMandatory = false;
    role.setMandatory = function(mandatory) {
      if (this.isMandatory == mandatory) return;
      if (!mandatory) {
        role.mandatoryDot.remove();
        role.mandatoryDot = null;
      } else {
        var player_x = player.x();
        var player_y = player.y();
        var closest = role.closest(player_x, player_y);
        role.mandatoryDot = paper.circle(closest.x, closest.y, 5).attr({"stroke-width": 0, fill: "#808"});
      }
      role.isMandatory = mandatory;
    };
    role.toggleMandatory = function() {
      this.setMandatory(!this.isMandatory);
      // MESSAGE: Set role to this.isMandatory
    };

    /*--------------- Live and Die together ---------------*/
    role.translating = false;
    role.lock = null;
    role._real_translate = role.translate;
    role.translate = function(dx, dy) {
      if (role.translating) return;
      role.translating = true;
      this._real_translate(dx, dy);
      select_hl.translate(dx, dy);
      for (var c in connectors) {
	var connector = connectors[c];
	if (connector.player.constraint && !connector.player.constraint.isFirstRole(role))
	 continue;
	if (this.lock)
	  connector.translate((role.lock & 1) ? dx : 0, (role.lock & 2) ? dy : 0);
        connector.redraw();
      }
      // Mandatory role dot is moved in closest, see above
      //if (this.isMandatory) { this.mandatoryDot.translate(dx, dy); }
      role.translating = false;
    };
    role._real_remove = role.remove;
    role.remove = function() {
      // MESSAGE: Delete role from fact_type
      paper.revertFocus(this);
      // Remove external references first
      fact_type.removeRole(this);
      while (connectors.length > 0)
        connectors[0].remove();
      // If this was the last role of the fact type, it needs to be deleted also.
      if (fact_type.roles.length === 0)
	fact_type.remove();
      select_hl.remove();
      if (this.isMandatory) this.mandatoryDot.remove();
      role._real_remove();
    };
    role._real_toFront = role.toFront;
    role.toFront = function() {
      this._real_toFront();
      if (this.mandatoryDot) this.mandatoryDot.toFront();
      for (var c in connectors) {
	connectors[c].toFront();
	connectors[c].player.toFront();
      }
    };
    /*
    role._real_toBack = role.toBack;
    role.toBack = function() {
      for (var c in connectors)
        connectors[c].toBack();
      this._real_toBack();
      background.toBack();
    };
    */
    role._real_show = role.show;
    role.show = function() { role._real_show(); if (this.isSelected()) select_hl.show(); };
    role._real_hide = role.hide;
    role.hide = function() { role._real_hide(); select_hl.hide(); };
    role.redraw = function() {
      for (var c in connectors)
        connectors[c].redraw();
    };

    role.handleKey = function(k, e) {
      switch (k.str) {
      case 'u': role.lock = 0; break;
      case 'l': role.lock = 3; break;
      case 'v': role.lock = role.lock ^ 1; break;   // Lock connector width (so player.y is constant)
      case 'h': role.lock = role.lock ^ 2; break;   // Lock connector height (so player.x is constant)
      case 'm': role.toggleMandatory(); break;
      case '1': fact_type.uniqueness(role); break;
      default:
	return paper.handleKey(k, e);
      }
      for (var c in connectors)
        connectors[c].highlight();
      return true;
    };

    /*--------------- Object highlighting ---------------*/
    role.highlight = function() {
      role.attr({fill: "#DDD"});
      var help = "";
      if (!role.isSelected())
	help += "(Shift-)Click to select role";
      if (role.fact_type.roles.length > 1)
	help += ", Double-click = mandatory, 1 = unique, shift-drag to reorder, ";
      else
	help += ", shift-drag to ";
      help += "reconnect or constrain, lock l/h/v/u";
      paper.showHelp(help.replace(/^, /,''));
      paper.takeFocus(this);
      for (var c in connectors)
        connectors[c].highlight();
    };
    role.unhighlight = function() {
      role.attr({fill: "#FFF"}); 
      paper.showHelp("");
      paper.revertFocus();
      for (var c in connectors)
        connectors[c].unhighlight();
    };
    role.changing = function() {  // Indicate when a role is about to move
      role.attr({"stroke-dasharray": "."});
    };
    role.unchanging = function() {
      role.attr({"stroke-dasharray": " "}); // VML
      role.attr({"stroke-dasharray": ""});  // SVG
    };

    /*--------------- Drag and drop ---------------*/
    // Drag the fact type when the role is dragged.
    // role.draggable({drag_obj: fact_type});
    role.draggable();
    role.dragStart = function(x, y, down, move) {
      if (down.shiftKey) {
	var dragged_roles = [];
	var add_this = true;
	var i;
	for (i in selected_objects) {
	  var selected_role = selected_objects[i];
	  if (selected_role.role) {
	    if (selected_role.role == role)
	      add_this = false;
	    dragged_roles.unshift(selected_role);
	  }
	}
	if (add_this)
	  dragged_roles.push(role);
	paper.deselectAll();
	var switched = false; // True when role will be re-ordered
	var role_box = role.getBBox();	// Save the starting role box
	var fact_box = fact_type.getBBox();
	var rubber_bands = [];
	for (i in dragged_roles) {
	  var tr = dragged_roles[i];
	  var r = rubber_bands[i] = paper.RubberBand(tr.x(), tr.y(), x, y, null, null);
	  r.attr({stroke: "#808", "stroke-width": 1, "stroke-dasharray" : "."});
	  r.toFront();
	}
	role.toFront();

	return {
	  dragUpdate: function(dragging_over, dx, dy, event) {
	    for (var i in rubber_bands) {
	      var rubber_band = rubber_bands[i];
	      var tr = dragged_roles[i];
	      rubber_band.dragUpdate(dragging_over, dx, dy, event);
	      var x = rubber_band.x(), y = rubber_band.y();
	      var c = tr.closest(x, y, true);
	      rubber_band.resetOrigin(c.x, c.y);
	      if (dragged_roles.length == 1
	       && x >= fact_box.x && x < fact_box.x+fact_box.width && y >= fact_box.y && y < fact_box.y+fact_box.height) {
		var switch_now = x < role_box.x || x >= role_box.x+role_box.width || y < role_box.y || y >= role_box.y+role_box.height;
		if (switch_now != switched)
		  if (!switched)
		    role.changing();
		  else
		    role.unchanging();
		switched = switch_now;
		rubber_band.hide();
	      } else {
		if (switched) {
		  switched = false;
		  role.unchanging();
		}
	      }
	    }
	  },
	  dragCancel: function() {
	    for (var i in rubber_bands)
	      rubber_bands[i].dragCancel();
	    if (switched) role.unchanging();
	  },
	  dragFinish: function(dropped_on, x, y, event) {
	    for (var i in rubber_bands)
	      rubber_bands[i].dragFinish(dropped_on, x, y, event);
	    if (switched) {
	      role.unchanging();
	      var target_role = dropped_on.raphael.role;
	      if (target_role && target_role.fact_type === role.fact_type) {
		var moved_num = fact_type.roleNum(role);
		var rn = fact_type.roleNum(target_role);
		role.fact_type.moveRoleTo(role, rn);
		// MESSAGE: Re-order roles in fact type, moving moved_num to rn
	      }
	    } else if (dropped_on == background.node) {
	      paper.Constraint(x, y, dragged_roles);
	      // MESSAGE: New constraint connected to dragged_roles
	    }
	    else if (dropped_on.raphael.constraint) {
	      dropped_on.raphael.constraint.addRoleSequence(dragged_roles);
	      // MESSAGE: Add role set to constraint, dragged_roles 
	    }
	    else if (dropped_on.raphael.objectType && dragged_roles.length == 1) {
	      connectors[0].changePlayer(dropped_on.raphael.objectType);
	      // MESSAGE: Reconnect role from old player to new player
	    }
	  },
	  hide: function() {
	    for (var i in rubber_bands)
	      rubber_bands[i].hide();
	    role.hide();
	  },
	  show: function() {
	    for (var i in rubber_bands)
	      rubber_bands[i].show();
	    role.show();
	  }
	};
      }
      return fact_type;	// Just drag the fact type if the shift key isn't pressed
    };

    /*--------------- Event Handlers ---------------*/
    role.hover(function() { role.highlight(); }, function() { role.unhighlight(); });
    role.click(function(event) { role.select(event.shiftKey); return false; });
    role.dblclick(function(event) { role.toggleMandatory(); return false; });
    role.highlight();
    return role;
  };

  /*
   * A Fact Type, basically just an array of roles
   */
  paper.FactType = function(first_player, x, y) {
    var paper = this;
    var fact_type = paper.rect(0, 0, 0, 0); // The background highlight rectangle is the fact_type object
    fact_type.attr({fill: "#DDD", "fill-opacity": 0, stroke: "#888", "stroke-width": 0, "stroke-dasharray" : "."});
    fact_type.fact_type = fact_type;  // For drop targetting
    fact_type.objectType = null;      // Not objectified
    fact_type.text = null;
    fact_type.text_x = 0;
    fact_type.text_y = 0;
    var roles = [];
    fact_type.roles = roles;

    var dragged_from_x, dragged_from_y;
    // Readings text object
    var readings = paper.TextEdit(x, y+24, first_player.name+" is").attr({"font-size" : 12});
    fact_type.readings = readings;
    fact_type.readings_x = 0;
    fact_type.readings_y = 0;
    readings.draggable();
    readings.cursor.draggable({drag_obj: readings});
    readings.background.draggable({drag_obj: readings});
    readings.dragStart = function(x, y, event) {
      if (readings.activated) return null;
      dragged_from_x = fact_type.readings_x;
      dragged_from_y = fact_type.readings_y;
      return readings;
    };
    readings.dragUpdate = function(dragging_over, dx, dy, event) {
      readings.translate(dx, dy);
      fact_type.readings_x += dx;
      fact_type.readings_y += dy;
    };
    readings.dragFinish = function(dragging_over, dx, dy, event) {
      // MESSAGE: Move fact_type readings text from dragged_from_x, dragged_from_y to fact_type.readings_x, fact_type.readings_y
    };

    var connectors = [];
    var uniqueness_constraints = [];
    fact_type.uniqueness_constraints = uniqueness_constraints;

    fact_type.getBBox = function() {
      var bb = roles[0].getBBox();
      for (var r in roles) {
        var b = roles[r].getBBox();
        if (bb.x > b.x) { bb.x = b.x; }
        if (bb.y > b.y) { bb.y = b.y; }
        if (bb.width < b.x+b.width-bb.x) { bb.width = b.x+b.width-bb.x; }
        if (bb.height < b.y+b.height-bb.y) { bb.height = b.y+b.height-bb.y; }
      }
      if (this.uniqueness_constraints.length > 0) {
	var b2 = this.uniqueness_constraints[this.uniqueness_constraints.length-1].getBBox();
	bb.height += bb.y-b2.y;
	bb.y = b2.y;
      }
      return bb;
    };

    fact_type.getOutline = function() {
      var bb = fact_type.getBBox();
      if (this.objectType)
        return {x: bb.x-15, y: bb.y-14, width: bb.width+30, height: bb.height+23};
      else
        return {x: bb.x-6, y: bb.y-6, width: bb.width+12, height: bb.height+12};
    };

    // Make the fact type encompass all roles with a margin
    fact_type.resize = function() {
      var bb = fact_type.getBBox();
      fact_type.attr(this.getOutline());

      if (!fact_type.removing) fact_type.restack_uniqueness_constraints();
      fact_type.toBack();
      background.toBack();
      for (var c in connectors)
        connectors[c].redraw();
      if (this.text)
	this.text.attr({x: bb.x+bb.width/2+fact_type.text_x, y: bb.y+bb.height/2+fact_type.text_y-40});
    };

    fact_type.x = function() { var b = this.getBBox(); return b.x+b.width/2; };
    fact_type.y = function() { var b = this.getBBox(); return b.y+b.height/2; };

    fact_type.roles = roles;
    fact_type.roleNum = function(role) {
      for (var r in roles) {
        if (roles[r] == role) return +r;
      }
      return null;
    };
    fact_type.adjacentRoles = function(r1, r2) {
      var p1 = this.roleNum(r1),
	  p2 = this.roleNum(r2);
      return p1 !== null && p2 !== null && (p1 == p2+1 || p2 == p1+1);
    };

    fact_type.setName = function(name) {
      this.name = name.replace(/ *!/g,'').replace(/[ \n]*\(.*/, '');
    };

    fact_type.objectify = function() {
      this.objectType = this;
      this.attr({r: 10});
      this.attr({stroke: "#009", "stroke-width": 3, "stroke-dasharray": "", fill: "#FFF", "fill-opacity": 1});
      this.attr({"stroke-dasharray": " "});
      this.readings.translate(0, 8);	// Extra space when objectified
      var name = "";
      for (var i in this.roles)
	name += this.roles[i].player.name;
      if (name === "" || typeof name === 'undefined')
	name = "New";
      this.setName(name);
      var text = paper.ObjectNameEdit(this.x(), this.y()-40, name, ' ', '"');
      text.resized = function() {
	fact_type.setName(text.value());
	// MESSAGE: Rename fact_type to fact_type.name
      };
      text.draggable();
      text.cursor.draggable({drag_obj: text});
      text.background.draggable({drag_obj: text});
      var textDragStart = function(x, y, event) {
	  if (text.activated) return null;
	  dragged_from_x = fact_type.text_x;
	  dragged_from_y = fact_type.text_y;
	  return text;
	};
      text.background.dragStart = textDragStart;
      text.cursor.dragStart = textDragStart;
      text.dragStart = textDragStart;
      text.dragUpdate = function(dragging_over, dx, dy, event) {
	text.translate(dx, dy);
	fact_type.text_x += dx;
	fact_type.text_y += dy;
      };
      text.dragFinish = function(dropped_on, x, y, event) {
	// MESSAGE: Move objectification name from dragged_from_x, dragged_from_y to fact_type.text_x, fact_type.text_y
      };
      text.objectType = fact_type;
      this.text = text;
      this.text_x = 0;
      this.text_y = 0;
      this.resize();
      this.deselect();
      this.text.activate();
      this.addEventHandlers();
    };
    fact_type.unobjectify = function() {
      this.deselect();
      this.objectType = null;
      this.attr({r: 0, stroke: "#888", "stroke-width": 0, "stroke-dasharray": ".", "fill-opacity": 0, fill: "#DDD"});
      this.resize();
      this.readings.translate(0, -8);
      this.text.deselect();
      this.text.remove();
      this.text = null;
      // REVISIT: For some reason MSIE doesn't show the fact type (used as hover highlight) after this.
    };
    fact_type.toggleObjectification = function() {
      if (!fact_type.objectType)
	fact_type.objectify();
      else
	fact_type.unobjectify();
      // MESSAGE: (un)objectify fact type as fact_type.name at fact_type.text_x, fact_type.text_y
    };

    /*--------------- Roles ---------------*/
    fact_type.addRole = function(player, adjacent_role, x, y) {
      // role might be the fact type itself, not the role
      var new_role;
      if (roles.length === 0) {
        new_role = paper.Role(this, player, x, y);
        roles.push(new_role);
	paper.RoleConnector(player, new_role).toBack();
	background.toBack();
      } else {
        var roleNum;   // The sequence position of the new role
        var r = this.roleNum(adjacent_role);
        if (r !== null) {
          adjacent_role.unhighlight();
          var abb = adjacent_role.getBBox();
          roleNum = r + ((x < abb.x+abb.width/2) ? 0 : 1);
	  // log("roleNum("+roleNum+") = adj("+r+") + (x("+x+") < m("+(abb.x+abb.width/2)+") ? 0 : 1");
        } else {
	  this.roleNum(adjacent_role);
          var fbb = this.getBBox();
          if (x < fbb.x+fbb.width/2) { roleNum = 0; } else { roleNum = roles.length; }
        }
	this.addRoleAt(player, roleNum);
	if (roles.length <= 2)
	  this.uniqueness(null);
      }
      this.restack_uniqueness_constraints();
      this.resize();
      return new_role;
    };
    fact_type.addRoleAt = function(player, roleNum) {	// Add a role to a fact type that has one already
      var new_x = roles[0].attr("x")+RoleWidth*roleNum; // x offset for new role
      this.translating = true;
      for (var r1 in roles)	// Move subsequent roles over
	if (+r1 >= roleNum)
	  roles[r1].translate(RoleWidth, 0);
      this.translating = null;
      var new_role = paper.Role(this, player, new_x+RoleWidth/2, roles[0].attr("y")+RoleHeight/2);
      roles.splice(roleNum, 0, new_role);
      paper.RoleConnector(player, new_role).toBack();
      background.toBack();
      for (r1 in roles)	// Move subsequent roles over
	roles[r1].redraw();
      this.resize();
    };
    fact_type.removeRole = function(role, leave_ucs) {
      var n = this.roleNum(role);
      if (n === null) return;
      var i;
      this.translating = true;
      for (i = n; i < roles.length-1; i++) {
	roles[i] = roles[i+1];	// Shuffle down one
	roles[i].translate(-RoleWidth, 0);
	roles[i].redraw();
      }
      this.translating = null;
      roles.pop();
      if (!leave_ucs) {
	for (i = 0; i < this.uniqueness_constraints.length; i++) {
	  var uc;
	  if ((uc = this.uniqueness_constraints[i]).excluded_role == role)
	  {
	    this.uniqueness_constraints.splice(i, 1);
	    // MESSAGE: Delete internal uniqueness constraint uc
	    uc.remove();
	    break;
	  }
	}
      }
      this.restack_uniqueness_constraints();
      if (roles.length == 1)
	this.uniqueness(null);
      if (roles.length)
	this.resize();
    };
    fact_type.moveRoleTo = function(role, roleNum) {
      var x = roles[roleNum].attr("x");
      this.removeRole(role, true);  // Remove it and close the gap
      this.translating = true;
      for (var r1 in roles)   // Move subsequent roles over
	if (+r1 >= roleNum)
	  roles[r1].translate(RoleWidth, 0);
      roles.splice(roleNum, 0, role);
      role.translate(x-role.attr("x"), 0);
      this.translating = null;
      for (r1 in roles)
	roles[r1].redraw();
      this.restack_uniqueness_constraints();
      this.resize();
    };

    /*--------------- Connectors ---------------*/
    fact_type.connect = function(connector) {
      connectors.push(connector);
    };
    fact_type.disconnect = function(connector) {
      array_delete(connectors, connector);
    };

    fact_type.roleJunction = function(roles, px, py) {
      var x0 = roles[0].attr("x");
      var x1 = roles[1].attr("x");
      var x = x0 < x1 ? x0 : x1;
      var top = roles[0].attr("y");

      return {x: x+RoleWidth, y: py < top+RoleHeight/2 ? top : top+RoleHeight};
    };

    /*--------------- Constraints ---------------*/
    fact_type.uniqueness = function(role) {	// Toggle uniqueness over role (null means all)
      /*
       * Either we have a spanning UC (uniqueness_constraints.length == 1 && uniqueness_constraints[0].excluded_role === null),
       * or we have one or more UCs with excluded_role != null
       */
      var uc;
      var deleted = false;
      for (var ucn = 0; ucn < this.uniqueness_constraints.length; ucn++) {
	uc = this.uniqueness_constraints[ucn];
	if (uc.excluded_role == role || (role && !uc.excluded_role) || this.roles.length <= 1) {
	  // Found existing, remove it.
	  this.uniqueness_constraints.splice(ucn, 1);
	  if (uc.excluded_role) deleted = true;
	  uc.remove();
	  this.restack_uniqueness_constraints();
	  this.resize();
	  break;
	}
      }
      if (this.roles.length == 1) return; // Don't do any more for unary fact types
      if (deleted && this.uniqueness_constraints.length > 0) return;
      if (role === null || deleted) role = null;	// We deleted the last non-spanning, or want a spanning UC

      // MESSAGE: Add internal uniqueness constraint covering all roles except "role"
      uc = paper.InternalUniqueness(fact_type, role);
      uc.row = 0;
      this.uniqueness_constraints.push(uc);
      this.restack_uniqueness_constraints();
      this.resize();
    };
    fact_type.restack_uniqueness_constraints = function() {
      var uc;
      for (var i = 0; i < this.uniqueness_constraints.length; i++)
	this.uniqueness_constraints[i].redraw();
      if (this.uniqueness_constraints.length > 2 || this.roles.length > 2) {
	for (i = 0; i < this.uniqueness_constraints.length; i++) {
	  uc = this.uniqueness_constraints[i];
	  uc.translate(0,i*-7);
	  uc.row = i;
	}
      }
    };

    /*--------------- Live and Die together ---------------*/
    fact_type._real_translate = fact_type.translate;
    fact_type.translate = function(dx, dy) {
      if (fact_type.translating) return;
      fact_type.translating = true;
      fact_type._real_translate(dx, dy);
      if (this.select_hl) this.select_hl.translate(dx, dy);
      for (var r in roles)
        roles[r].translate(dx, dy);
      for (var uc in this.uniqueness_constraints)
	this.uniqueness_constraints[uc].translate(dx, dy);
      // Update this end-point of any role connectors
      for (var c in connectors) {
	var connector = connectors[c];
	var lock = connector.role.lock;
	if (lock)
	  connector.role.fact_type.translate((lock & 1) ? dx : 0, (lock & 2) ? dy : 0);
        connector.redraw();
      }
      if (this.text)
	this.text.translate(dx, dy);
      this.readings.translate(dx, dy);
      fact_type.translating = null;
    };
    fact_type._real_toFront = fact_type.toFront;
    fact_type.toFront = function() {
      this._real_toFront();
      this.readings.toFront();
      if (this.select_hl) this.select_hl.toFront();
      for (var r in roles) 
        roles[r].toFront();
      for (var uc in this.uniqueness_constraints)
	this.uniqueness_constraints[uc].toFront();
      if (this.text)
	this.text.toFront();
    };
    fact_type._real_toBack = fact_type.toBack;
    fact_type.toBack = function() {
      // The front-most things must be put back first:
      if (this.select_hl) this.select_hl.toBack();
      for (var r in roles) 
        roles[r].toBack();
      for (var uc in this.uniqueness_constraints)
	this.uniqueness_constraints[uc].toBack();
      for (var c in connectors)
	if (!connectors[c].player.constraint)
	  connectors[c].player.toBack();
      if (this.text)
	this.text.toBack();
      this.readings.toBack();
      this._real_toBack();
      background.toBack();
    };
    fact_type._real_remove = fact_type.remove;
    fact_type.removing = false;
    fact_type.remove = function() {
      // MESSAGE: Delete fact_type. REVISIT: This will also send messages removing roles and UCs etc; avoid this?
      paper.revertFocus(this);
      if (this.removing) return;  // Recursion control
      this.removing = true;
      if (this.text)
	this.text.remove();
      for (var uc in this.uniqueness_constraints)
	this.uniqueness_constraints[uc].remove();
      while (roles.length > 0)
        roles[0].remove();
      if (this.select_hl) this.select_hl.remove();
      for (var c in connectors) {
	if (connectors[c].role)
	  connectors[c].role.remove();    // Otherwise just delete the role
	else
	  connectors[c].remove();
      }
      this.readings.remove();
      fact_type._real_remove();
    };

    /*--------------- Drag and drop ---------------*/
    fact_type.dragStart = function(x, y, event) {
      dragged_from_x = this.x();
      dragged_from_y = this.y();
      if (fact_type.objectType && fact_type.text.isSelected())
	fact_type.text.deselect();
      if (event.shiftKey) {
	if (!fact_type.objectType) return null;	// Don't drag
        paper.deselectAll();
	var rb = paper.RubberBand(
	  fact_type.x(), fact_type.y(),
	  x, y,
	  function(dragging_over, dx, dy, event) {
	    if (!dragging_over) return;
	    var target = dragging_over.raphael;
	    if (dragging_over == background.node)
	      paper.showHelp("Drop here to create a new fact type");
	    else if (target.objectType && target.objectType != fact_type.objectType)
	      paper.showHelp("Drop here to connect to this supertype");
	    else if (target && target.fact_type)
	      paper.showHelp("Drop here to add a role");
	    else
	      paper.showHelp("");
	  },
	  function(dropped_on, x, y, e) {
	    if (!dropped_on)
	      return;
	    else if (dropped_on == background.node) {
	      // MESSAGE: New unary fact type with this objectified fact type as player, readings at fact_type.readings_x, y
	      paper.FactType(fact_type, x, y);
	    } else if (dropped_on.raphael) {
	      var target = dropped_on.raphael;
	      if (target.fact_type) {
		target.fact_type.addRole(fact_type, target, x, y);
		// MESSAGE: Add role to fact type. (which role?)
	      } else if (target.objectType && target.objectType != fact_type.objectType) {
		target.objectType.supertype_of(fact_type.objectType);
		// MESSAGE: New type inheritance, target.objectType is a supertype of fact_type.object_type
		// REVISIT: There is no way to remove a subtyping relationship
	      }
	      else
		log("Can't connect fact type to "+target);
	    }
	    //else // Drop unsuccessful; but later, detect drops on external constraint objects
	  }
	);
	rb.toFront();
	fact_type.toFront();
	return rb;
      }
      fact_type.toFront();

      return this;
    };
    fact_type.dragUpdate = function(dragging_over, dx, dy, event) {
      if (fact_type.isSelected()) {
	for (var i in selected_objects) {
	  selected_objects[i].translate(dx, dy);
	}
      } else
	fact_type.translate(dx, dy);
    };
    fact_type.dragFinish = function(dropped_on, x, y, event) {
      // MESSAGE: Move fact_type (with selected_objects) from dragged_from_x, dragged_from_y to this.x(), this.y()
    };

    /*--------------- Subtyping ---------------*/
    fact_type.supertype_of = function(subtype) {
      paper.SubtypeConnector(this, subtype);
    };
    fact_type.closest = function(ex, ey) {
      var bb = this.getOutline();
      return IntersectRoundRect(ex, ey, bb.x+bb.width/2, bb.y+bb.height/2, bb, 10);
    };

    /*--------------- Object selection ---------------*/
    fact_type.isSelected = function() {
      return paper.isSelected(this);
    };
    fact_type.select = function(add) {
      if (this.isSelected()) { return; }
      if (!add) { paper.deselectAll(); }
      if (this.objectType) {
	this.select_hl = paper.rect(    // An object to show this object type is selected
	  this.attr("x")+4, this.attr("y")+4, this.attr("width")-8, this.attr("height")-8, 6
	).attr({stroke: "#888", "stroke-width": 2, "stroke-dasharray" : "."});
	this.select_hl.fact_type = this;
	this.select_hl.objectType = this;
	this.select_hl.draggable({drag_obj: fact_type, right_button: false});
	this.select_hl.hover(function() { fact_type.highlight(); }, function() { fact_type.unhighlight(); });
      } else
	this.attr({"stroke-width": 1});
      paper.select(this);
      for (var r in roles)
        roles[r].deselect();
    };
    fact_type.deselect = function() {
      if (this.objectType) {
	if (this.select_hl) this.select_hl.remove();
	this.select_hl = null;
      } else
	this.attr({"stroke-width": 0});
      paper.deselect(this);
    };

    /*--------------- Object highlighting ---------------*/
    fact_type.highlight = function() {
      fact_type.attr({fill: "#DDD", "fill-opacity": 1});
      for (var c in connectors)
        connectors[c].highlight();
      if (fact_type.objectType)
	if (!fact_type.isSelected())
	  paper.showHelp("Click to select fact type, shift-drag to connect");
	else
	  paper.showHelp("Shift-drag to connect");
      else if (!fact_type.isSelected())
	paper.showHelp("Click to select fact type");
    };
    fact_type.unhighlight = function() {
      if (this.objectType)
	fact_type.attr({"fill-opacity": 1, fill: "#FFF"});
      else
	fact_type.attr({"fill-opacity": 0});
      for (var c in connectors)
        connectors[c].unhighlight();
      paper.showHelp("");
    };

    /*--------------- Event Handlers ---------------*/
    fact_type.addEventHandlers = function() {
      fact_type.draggable({right_button: false});
      fact_type.hover(function() { fact_type.highlight(); }, function() { fact_type && fact_type.unhighlight(); });
      fact_type.dblclick(function(event) { fact_type.toggleObjectification(); });
      fact_type.click(function(event) { fact_type.select(event.shiftKey); return false; });
    };

    fact_type.addEventHandlers();
    fact_type.addRole(first_player, null, x, y);

    return fact_type;
  };

  paper.InternalUniqueness = function(fact_type, excluded_role) {
    var uc = paper.path("M0 0");
    uc.excluded_role = excluded_role;
    uc.deontic = false;
    uc.is_preferred = false;
    uc.select_hl = paper.rect(0, -3, RoleWidth, 5).attr({fill: "#DDD", "fill-opacity": 0, "stroke-width": 0});
    uc.select_hl.toBack();
    background.toBack();
    uc.objectType = function() { return fact_type.objectType(); }; // For drop targetting

    /*--------------- Set path and colour ---------------*/
    uc.setDeontic = function(deontic) {
      this.deontic = deontic;
      fact_type.restack_uniqueness_constraints();
    };
    uc.setPreferred = function(pref) {
      this.is_preferred = pref;
      if (pref)
	this.setDeontic(false);
      fact_type.restack_uniqueness_constraints();
    };
    uc.redraw = function() {
      var r = fact_type.roles;
      var xrnum = fact_type.roleNum(excluded_role);
      var x = 2; // Offset of left-hand end
      if (xrnum === 0)
	x += RoleWidth;
      if (this.deontic_ring) this.deontic_ring.remove();
      this.deontic_ring = null;
      if (this.deontic) {
	this.deontic_ring = paper.circle(x+2, 0, 2)
	  .attr({"stroke-width": 1, stroke: "#00A", fill: "#FFF"});
	x += 4; // skip diameter of circle
      }
      var path = "M"+x+" 0";

      x = (!excluded_role || xrnum === 0 ? r.length : xrnum)*RoleWidth;
      path += "L"+(x-2)+" 0";
      if (xrnum > 0 && xrnum+1 < r.length) {
	if (xrnum > 0) {
	  // Two dashes to skip a role
	  for (var i = 4; i < RoleWidth; i += 9)
	    path += "M"+(x+i)+"\n0L"+(x+i+5)+"\n0"; // newlines prevent the regex below
	}
	// The line following the excluded role:
	path += "M"+((xrnum+1)*RoleWidth+2)+" 0L"+(r.length*RoleWidth-2)+" 0";
      }
      if (this.is_preferred)	// Double it if the UC is the preferred ID.
	path = path.replace(/ 0/g, '-2') + path.replace(/ 0/g, ' 1');

      this.attr({path: path, stroke: this.deontic ? "#00A" : "#808", "stroke-width": 2});
      var paired = fact_type.uniqueness_constraints.length == 2 && fact_type.roles.length == 2;
      this.select_hl.attr({
	x: !paired || xrnum !== 0 ? 0 : RoleWidth,
	y: this.is_preferred ? -4 : -2,
	width: (paired ? 1 : r.length)*RoleWidth,
	height: this.is_preferred ? 7 : 5
      });
      var dx = r[0].attr("x"), dy = r[0].attr("y")-5;
      this.translate(dx, dy);
    };

    /*--------------- Live and Die together ---------------*/
    uc._real_translate = uc.translate;
    uc.translate = function(dx, dy) {
      this._real_translate(dx, dy);
      if (this.select_hl)
	this.select_hl.translate(dx, dy);
      if (this.deontic_ring)
	this.deontic_ring.translate(dx, dy);
    };
    uc._real_toFront = uc.toFront;
    uc.toFront = function() {
      if (this.select_hl) this.select_hl.toFront();
      this._real_toFront();
      if (this.deontic_ring) this.deontic_ring.toFront();
    };
    uc._real_toBack = uc.toBack;
    uc.toBack = function() {
      if (this.deontic_ring) this.deontic_ring.toBack();
      this._real_toBack();
      if (this.select_hl) this.select_hl.toBack();
      background.toBack();
    };

    uc._real_remove = uc.remove;
    uc.remove = function() {
      // MESSAGE: Delete this internal uniqueness constraint
      paper.revertFocus(this);
      if (this.select_hl) this.select_hl.remove();
      if (this.deontic_ring) this.deontic_ring.remove();
      this._real_remove();
    };

    uc.highlight = function() {
      uc.select_hl.attr({"fill-opacity": 1});
      paper.showHelp("p -> preferred, d -> deontic");
      paper.takeFocus(this);
    };
    uc.unhighlight = function() {
      uc.select_hl.attr({"fill-opacity": 0});
      paper.showHelp("");
      paper.revertFocus();
    };
    uc.handleKey = function(k, e) {
      switch (k.str) {
      case 'p': uc.setPreferred(!uc.is_preferred); break;
      case 'd': if (!uc.is_preferred) uc.setDeontic(!uc.deontic); break;
      default:
	return false;
      }
      // MESSAGE: Change internal uniqueness constraint preferred/deontic state
      return true;
    };

    uc.hover(function() { uc.highlight(); }, function() { uc.unhighlight(); });
    uc.select_hl.hover(function() { uc.highlight(); }, function() { uc.unhighlight(); });

    uc.redraw();
    return uc;
  };

  paper.CrossHairs = function(x, y, width, height) {
    // Crosshairs:
    if (typeof width == 'undefined') { width = 60; }
    if (typeof height == 'undefined') { height = 60; }
    var vert = this.path("M"+x+" "+(y-height/2)+"L"+x+" "+(y+height/2));
    var horiz = this.path("M"+(x-width/2)+" "+y+"L"+(x+width/2)+" "+y);

    /*--------------- Live and Die together ---------------*/
    vert._real_translate = vert.translate;
    vert.translate = function(dx, dy) {
      vert._real_translate(dx, dy);
      horiz.translate(dx, dy);
    };
    vert._real_remove = vert.remove;
    vert.remove = function() {
      horiz.remove();
      // Unregister event bindings?
      vert._real_remove();
    };

    return vert;
  };

  /*
   * An editable text widget.
   * This consists of the text object (returned), a background rectangle, and a cursor rectangle.
   */
  paper.TextEdit = function(x, y, value) {
    // for left alignment: 'text-anchor': 'start'
    var text = this.text(x, y, value).attr({"font-size" : 16});
    text.textEdit = text;
    text.background = paper.rect().
      attr({stroke: "#000", fill: "#DDD", "fill-opacity": 0, "stroke-width": 0, "stroke-dasharray": "-"});
    text.cursor = paper.rect().attr({fill: "#88F", stroke: "#FFF", "stroke-width": 0}).hide();

    var x_offset = $.browser.msie ? -3 : -6;
    var y_offset = $.browser.msie ? -4 : -1;
    var x_margin = $.browser.msie ? 8 : 10;
    var y_margin = 3;
    if ($.browser.opera) { y_offset = -4; y_margin = 6; }
    text.x_offset = x_offset;
    text.y_offset = y_offset;
    text.x_margin = x_margin;
    text.y_margin = y_margin;

    text.resized = function() {};
    text.value = function() {
      return this.attr('text');
    };
    text.setText = function(str) {
      if (str === '') {
	str = '_';
	text.cursorLeft = 0;
	text.cursorRight = 1;
      }
      text.attr({text: str});
      var bbox = text.getBBox();
      this.background.attr({
	x: bbox.x+x_offset, y: bbox.y+y_offset,
	width: bbox.width+x_margin, height: bbox.height+y_margin
      });
      // Force the cursor inside the new length
      if (this.cursorRight > str.length)
	this.cursorRight = str.length;
      if (this.cursorLeft > this.cursorRight)
	this.cursorLeft = this.cursorRight;
      this.reshapeCursor();
      this.resized();
    };

    /*--------------- Cursor ---------------*/
    text.cursorLeft = 0;    // character position of first selected character
    text.cursorRight = 0;   // character position after last selected character
    text.reshapeCursor = function() {
      var bb = this.getBBox();
      var x1 = text.widthToCharNum(text.cursorLeft);
      var x2 = text.widthToCharNum(text.cursorRight);
      text.cursor.attr({
	x: bb.x+x1+x_offset+4, y: bb.y+y_offset+2,
	width: (x2 == x1 ? 2 : x2-x1+2), height: bb.height+text.y_margin-4
      });
    };
    text.positionCursor = function(charNum) {
      var charCount = text.attr("text").length;
      if (charNum > charCount)
	charNum = charCount;
      else if (charNum < 0)
	charNum = 0;
      this.cursorLeft = this.cursorRight = charNum;
      this.reshapeCursor();
    };
    text.selectAll = function() {
      text.cursorLeft = 0;
      text.cursorRight = text.attr("text").length;
      text.select();
      text.reshapeCursor();
    };

    /*--------------- Keyboard input ---------------*/
    text.handleKey = function(k, e) {
      var str = text.attr("text");
      // log("text.handleKey, code="+k.code+", str="+k.str);
      //log("text handling keyCode="+k.code);
      if (k.str) {
	text.setText(str.slice(0, text.cursorLeft)+k.str+str.slice(text.cursorRight));
	text.cursorLeft++;
	text.cursorRight = text.cursorLeft;
	text.reshapeCursor();
      } else {
	var newtext;
	switch (k.code) {
	case Key.ENTER:  // Enter
	  text.deselect();  // This only works to edit single-line text
	  break;
	case Key.LEFT:  // cursor left
	  if (e.shiftKey) {
	    if (text.cursorLeft > 0) { text.cursorLeft--; }
	  } else {
	    text.cursorRight = text.cursorLeft;
	    if (text.cursorLeft > 0) { text.cursorLeft--; text.cursorRight--; }
	  }
	  text.reshapeCursor();
	  break;
	case Key.RIGHT:  // cursor right
	  if (e.shiftKey) {
	    if (text.cursorRight < str.length) { text.cursorRight++; }
	  } else {
	    text.cursorLeft = text.cursorRight;
	    if (text.cursorLeft < str.length) { text.cursorLeft++; text.cursorRight++; }
	  }
	  text.reshapeCursor();
	  break;
	case Key.DELETE:  // Delete forwards
	  if (text.cursorLeft == text.cursorRight && text.cursorRight < str.length) { text.cursorRight++; }
	  newtext = str.slice(0, text.cursorLeft)+str.slice(text.cursorRight);
	  text.cursorRight = text.cursorLeft;
	  text.setText(newtext);
	  text.reshapeCursor();
	  break;
	case Key.BACKSPACE:   // Backspace
	  if (text.cursorLeft == text.cursorRight && text.cursorLeft > 0) { text.cursorLeft--; }
	  newtext = str.slice(0, text.cursorLeft)+str.slice(text.cursorRight);
	  text.cursorRight = text.cursorLeft;
	  text.setText(newtext);
	  text.reshapeCursor();
	  break;
	case Key.END:
	  text.cursorLeft = text.cursorRight = str.length;
	  text.reshapeCursor();
	  break;
	case Key.HOME:
	  text.cursorLeft = text.cursorRight = 0;
	  text.reshapeCursor();
	  break;
	case Key.SHIFT:	  // Shift
	  return true;	  // Propagate the key
	default:
	  log("unhandled special keyCode="+k.code);
	  return true;    // The key was not handled
	}
      }
      return false;  // The key was handled
    };

    /*--------------- Selection ---------------*/
    text.isSelected = function() {
      return paper.isSelected(this);
    };
    text.deselect = function() {
      if (this.activated)
	this.deactivate();
      this.background.attr({"fill-opacity": 0, "stroke-width": 0});
      this.cursor.hide();
      paper.deselect(this);
      paper.revertFocus();
    };
    text.select = function(event) {
      if (event) {
	var position = paper.pageOffset();
        var found = text.locate({x: event.pageX-position.left, y: event.pageY-position.top});
	text.cursorLeft = text.cursorRight = found.charNum;
        text.reshapeCursor();
      } else {
        text.cursor.show();
	text.background.attr({"fill-opacity": 1, "stroke-width": 1});
      }
      paper.select(text);
      paper.takeFocus(this);
    };
    text.extendCursor = function(event) {
      var position = paper.pageOffset();
      var found = text.locate({x: event.pageX-position.left, y: event.pageY-position.top});
      if (text.cursorLeft >= found.charNum) {
        // Because we don't remember the original start location, this yields slightly assymetric results.
        // For example, drag left, then right again - it's as if you started from where you went left to.
        text.cursorLeft = found.charNum;
      } else {
        text.cursorRight = found.charNum;
      }
      text.reshapeCursor();
    };

    /*--------------- Live and Die together ---------------*/
    text._real_translate = text.translate;
    text.translate = function(dx, dy) {
      this._real_translate(dx, dy);
      text.background.translate(dx, dy);
      text.cursor.translate(dx, dy);
    };
    text._real_toFront = text.toFront;
    text.toFront = function() {
      text.background.toFront();
      text.cursor.toFront();
      this._real_toFront();
    };
    text._real_toBack = text.toBack;
    text.toBack = function() {
      this._real_toBack();
      text.cursor.toBack();
      text.background.toBack();
      background.toBack();
    };
    text._real_remove = text.remove;
    text.remove = function() {
      paper.revertFocus(this);
      text.background.remove();
      text.cursor.remove();
      // Unregister event bindings?
      text._real_remove();
    };

    /*--------------- Object highlighting ---------------*/
    text.highlight = function() {
      text.background.attr({"fill-opacity": 1});
      if (this.activated)
	paper.showHelp("Type a new name for this object type. Add ! for independent, (Mode) for reference mode");
      else
	paper.showHelp("Click to edit text, Double-click to replace");
    };
    text.unhighlight = function() {
      if (!text.isSelected())
	text.background.attr({"fill-opacity": 0});
      paper.showHelp("");
    };
    text.activate = function() {
      // Enter text-edit mode with all text selected
      paper.deselectAll();
      text.selectAll();
      text.activated = true;
    };
    text.deactivate = function() {
      text.activated = false;
    };

    /*--------------- Find character position from click ---------------*/
    text.widthToCharNum = function(charNum) {
      var charCount = text.attr("text").length;
      if (charNum > charCount)
	charNum = charCount;
      else if (charNum <= 0)
	return 0;
      if (window.Raphael.svg) {
	try {
	  // log('count='+charCount+" num="+charNum);
	  return text.node.getSubStringLength(0, charNum);
	} catch (e) {
	  return text.widthToCharNum(charNum-1);
	}
      } else {	// MSIE
	if (charNum === 0)
	  return 0; // First char
	// REVISIT: Can't do this yet on MSIE; use fixed-width assumption
	var bb = this.getBBox();
	return bb.width*charNum/charCount;
      }
    };
    text.posFromCharNum = function(charNum) {
      var bbox = this.getBBox();
      return bbox.x+text.widthToCharNum(charNum);
    };
    text.locate = function(pos) {
      var bbox = this.getBBox();
      var str = text.attr("text");
      var last = 0;
      var charNum = -1;
      for (var i = 0; i < str.length; i++) {
        var width = text.widthToCharNum(i);
        if (pos.x >= bbox.x+last && pos.x <= bbox.x+width) {
          if (pos.x-bbox.x-last > (width-last)/2) {
            charNum = i;
          } else {
            charNum = i-1;
          }
          break;
        }
        last = width;
      }
      if (charNum < 0) {
        if (pos.x < bbox.x) { charNum = 0; } else { charNum = str.length; }
      }
      var x = text.widthToCharNum(charNum)-1;
      return {x: x, top: 0, bottom: bbox.height, charNum: charNum};
    };

    /*--------------- Event Handlers ---------------*/
    text.draggable({reluctance:0});
    text.background.draggable({reluctance:0});
    text.dragStart = function(x, y, downEvent, moveEvent) {
      if (!text.activated) return null;
      downEvent = $.event.fix(downEvent);
      text.select(downEvent);
      return {
        dragUpdate: function(dragging_over, dx, dy, event) {
	  text.extendCursor(event);
	},
	dragFinish: function() { },
        hide: function() { },
        show: function() { }
      };
    };
    text.background.dragStart = text.dragStart;

    text.dblclick(function(event) { text.activate(); return false; });
    text.background.dblclick(function(event) { text.activate(); return false; });

    text.hover(function() { text.highlight(); }, function() { text.unhighlight(); });
    text.background.hover(function() { text.highlight(); }, function() { text.unhighlight(); });

    text.setText(value);
    text.deselect();
    text.toFront();
    return text;
  };

  paper.ObjectNameEdit = function(x, y, value, sep, quote) {
    var t = paper.TextEdit(x, y, value);
    t.orig_activate = t.activate;
    t.orig_deactivate = t.deactivate;
    t.activate = function() {
      var edit_value = t.value()
	  .replace(/["']+/g, '')
	  .replace(/[\n ]+/g, ' ')
	  .replace(new RegExp(quote, 'g'), '');
      edit_value = edit_value.replace(/ *! */, "!");
      t.setText(edit_value);
      t.orig_activate();
      paper.showHelp("Type a new name for this object type. Add ! for independent, (Mode) for reference mode");
    };
    t.deactivate = function() {
      if (!quote) quote = '';
      if (!sep) sep = '\n';
      var display_value =
	quote
	+ t.value()
	  .replace(/[ \n]*\(\.?/g, sep+'(.');
      if (display_value.match(/!/)) {
	display_value = display_value.replace(/!/g, '');
	this.independent = true;
      }
      else
	this.independent = false;
      if (this.independent)
	display_value = display_value.replace(/(\n|\(|$)/, " !$1");
      display_value += quote;
      t.setText(display_value);
      t.orig_deactivate();
    };
    return t;
  };

  /*
   * A value type or entity type shape
   */
  paper.ObjectType = function(name, x, y) {
    var paper = this;

    var object_type = paper.rect(0,0,1,1,10); // The outer shape
    object_type.attr({stroke: "#009", "stroke-width": 3, fill: "#FFF"});
    object_type.attr({"stroke-dasharray" : " "});
    object_type.attr({"stroke-dasharray" : ""});
    object_type.objectType = object_type;
    object_type.setName = function(name) {
      this.name = name.replace(/ *!/g,'').replace(/[ \n]*\(.*/, '');
    };
    object_type.setName(name);

    var select_hl = paper.rect(0,0,1,1,6);    // An object to show this object type is selected
    select_hl.attr({stroke: "#888", "stroke-width": 2, "stroke-dasharray" : "."});
    select_hl.objectType = object_type;
    object_type.select_hl = select_hl;
    select_hl.hide();

    var text = paper.ObjectNameEdit(x, y, name);
    text.objectType = object_type;
    object_type.text = text;
    text.resized = function() {
      object_type.resize();
      object_type.setName(text.value());
    };

    object_type.resize = function() {
      var bbox = text.getBBox();

      this.attr({x: bbox.x+text.x_offset-7, y: bbox.y+text.y_offset-8, width: bbox.width+text.x_margin+14, height: bbox.height+text.y_margin+16});

      select_hl.attr({x: bbox.x+text.x_offset-7+4, y: bbox.y+text.y_offset-8+4, width: bbox.width+text.x_margin+14-8, height: bbox.height+text.y_margin+16-8});
    };
    object_type.resize();

    object_type.x = function() { var b = text.getBBox(); return b.x+b.width/2; };
    object_type.y = function() { var b = text.getBBox(); return b.y+b.height/2; };

    object_type.setText = function(name) {
      text.attr({text: name});
      this.setName(name);
      this.resize();
      // MESSAGE: Rename object_type
      this.node.id = name+"_id";
    };

    /*--------------- Connectors ---------------*/
    var connectors = [];
    object_type.connect = function(connector) {
      connectors.push(connector);
    };
    object_type.disconnect = function(connector) {
      array_delete(connectors, connector);
    };

    /*--------------- Live and Die together ---------------*/
    object_type._real_translate = object_type.translate;
    object_type.translating = null;
    object_type.translate = function(dx, dy) {
      if (object_type.translating) return;
      object_type.translating = true;
      this._real_translate(dx, dy);
      select_hl.translate(dx, dy);
      text.translate(dx, dy);
      // Update this end-point of any role connectors
      for (var c in connectors) {
	var connector = connectors[c];
	var lock = connector.role.lock;
	if (lock)
	  connector.role.fact_type.translate((lock & 1) ? dx : 0, (lock & 2) ? dy : 0);
        connector.redraw();
      }
      object_type.translating = null;
    };
    object_type._real_toFront = object_type.toFront;
    object_type.toFront = function() {
      this._real_toFront();
      select_hl.toFront();
      text.toFront();
    };
    object_type._real_remove = object_type.remove;
    object_type.remove = function() {
      // MESSAGE: Delete object_type
      paper.revertFocus(this);
      select_hl.remove();
      text.remove();
      for (var c in connectors) {
	if (connectors[c].role)
	  connectors[c].role.remove();	// This removes the role connectors
	else
	  connectors[c].remove();
      }
      this._real_remove();
    };

    /*--------------- Drag and drop ---------------*/
    var connecting_subclass = false;
    var dragged_from_x, dragged_from_y;
    object_type.dragStart = function(x, y, event) {
      dragged_from_x = this.x();
      dragged_from_y = this.y();
      if (!object_type.isSelected()) { paper.deselectAll(); }

      var to_deselect = [];
      for (var i in selected_objects) {
	var o = selected_objects[i];
	if (o.type == 'text')
	  to_deselect.push(o);	// There can only be one text object selected
      }
      for (i in to_deselect)
	to_deselect[i].deselect();

      if (text.isSelected()) text.deselect();

      connecting_subclass = event.metaKey;

      if (event.shiftKey) {
        paper.deselectAll();
	var b = text.getBBox();
	var rb = paper.RubberBand(
	  b.x+b.width/2, b.y+b.height/2,
	  x, y,
	  function(dragging_over, dx, dy, event) {
	    if (!dragging_over) return;
	    var target = dragging_over.raphael;
	    if (!target) return;
	    if (dragging_over == paper.canvas)
	      paper.showHelp("Drop here to create a new fact type");
	    else if (target.objectType && target.objectType != object_type)
	      paper.showHelp("Drop here to connect to this supertype");
	    else if (target && target.fact_type)
	      paper.showHelp("Drop here to add a role");
	    else
	      paper.showHelp("");
	  },
	  function(dropped_on, x, y, e) {
	    if (!dropped_on)
	      return;
	    else if (dropped_on == background.node) {
	      // MESSAGE: New fact type at x, y played by object_type, readings at fact_type.readings_x, y
	      paper.FactType(object_type, x, y);
	    } else if (dropped_on.raphael) {
	      var target = dropped_on.raphael;
	      if (target.fact_type && !(target.objectType && !target.role)) {
		// MESSAGE: Add a role to target.fact_type (at which position?)
		target.fact_type.addRole(object_type, target, x, y);
	      } else if (target.objectType && target.objectType != object_type) {
		// MESSAGE: New type inheritance, target.objectType is a supertype of object_type
		target.objectType.supertype_of(object_type);
		// REVISIT: There is no way to remove a subtyping relationship
	      } else
		// Drop on a constraint? Could that mean something?
		log("Can't connect object type to "+target);
	    }
	    //else // Drop unsuccessful; but later, detect drops on external constraint objects
	  }
	);
	rb.toFront();
	object_type.toFront();
	return rb;
      } else
        object_type.toFront();
      return this;
    };

    object_type.dragUpdate = function(dragging_over, dx, dy, event) {
      // log(object_type.node.id+" -> (dx,dy) = "+dx+","+dy+" over "+dragging_over);
      // log("ctrl="+event.ctrlKey+", shift="+event.shiftKey+", alt="+event.altKey);
      if (object_type.isSelected()) {
	for (var i in selected_objects) {
	  selected_objects[i].translate(dx, dy);
	}
      } else
	object_type.translate(dx, dy);
    };
    object_type.dragFinish = function(dropped_on, x, y, event) {
      // MESSAGE: Move object_type (with selected_objects) from dragged_from_x, dragged_from_y to this.x(), this.y()
    };
    object_type.draggable({right_button: false});
    select_hl.draggable({drag_obj: object_type, right_button: false});
    object_type.node.id = name+"_id";

    /*--------------- Subtyping ---------------*/
    object_type.supertype_of = function(subtype) {
      paper.SubtypeConnector(this, subtype);
    };
    object_type.closest = function(ex, ey) {
      var bb = this.getBBox();
      return IntersectRoundRect(ex, ey, bb.x+bb.width/2, y = bb.y+bb.height/2, bb, 10);
    };

    /*--------------- Object selection ---------------*/
    object_type.isSelected = function() {
      return paper.isSelected(this);
    };
    object_type.deselect = function() {
      this.select_hl.hide();
      paper.deselect(this);
    };
    object_type.select = function(add) {
      if (this.isSelected()) { return; }
      if (!add) { paper.deselectAll(); }
      select_hl.show();
      select_hl.toFront();	// Necessary in Opera only
      paper.select(this);
    };

    /*--------------- Object highlighting ---------------*/
    object_type.highlight = function() {
      this.attr({fill: "#DDD"});
      paper.showHelp(
        (!this.isSelected() ? "Click to select, " : "")+
        "Drag to move, Shift-drag to create a fact type or connect to supertype"
      );
    };
    object_type.unhighlight = function() {
      this.attr({fill: "#FFF"});
      paper.showHelp("");
    };
    object_type.activate = function() {
      // log("REVISIT: Pop up something to set the attributes (data type, identification mode, independence, etc)");
      // For now, switch an entity type into a value type and back
      if (this.attr('stroke-dasharray') == '-') {
	// Setting to an empty string is broken in Raphael on MSIE
        this.attr({"stroke-dasharray" : " "});
        this.attr({"stroke-dasharray" : ""});
      } else
        this.attr({"stroke-dasharray" : "-"});
      // MESSAGE: Change this object_type to/from a value type
    };

    /*--------------- Event Handlers ---------------*/
    object_type.hover(function() { object_type.highlight(); }, function() { object_type.unhighlight(); });
    select_hl.hover(function() { object_type.highlight(); }, function() { object_type.unhighlight(); });
    object_type.click(function(event) { object_type.select(event.shiftKey); return false; });
    select_hl.click(function(event) { return false; }); // stop click propagating to deselectAll
    object_type.dblclick(function(event) { object_type.activate(); });
    select_hl.dblclick(function(event) { object_type.activate(); });

    text.deactivate();
    text.activate();

    return object_type;
  };

  /*
   * External Constraints
   * The constraint object is the highlight, an ellipse (that's sometimes circular).
   * On top is drawn an "icon", which is a path.
   *
   * External constraints have differing expected numbers of roles or role sequences, as below:
   */
  var ConstraintKind = {
    Unknown: 0,
    Uniqueness: 1,  // One role sequence
    Preferred: 2,   // One role sequence
    Mandatory: 3,   // One role sequence
    Frequency: 4,   // One role sequence
    Exclusion: 5,   // Two or more role sequences
    ExOr: 6,	    // Two or more role sequences
    Equality: 7,    // Two or more role sequences
    Ring: 8,	    // Two roles exactly
    Subset: 9,	    // Two role sequences exactly
    Value: 10,	    // One ValueType or role exactly
    // RoleValue: 11,  // One role exactly
    MAX: 11
  };
  var RingKind = {
    Irreflexive: 1,
    Symmetric: 2,
    Asymmetric: 3,
    AntiSymmetric: 4,
    Intransitive: 5,
    Acyclic: 6,
    AcyclicIntransitive: 7,
    AsymmetricIntransitive: 8,
    SymmetricIntransitive: 9,
    SymmetricIrreflexive: 10,
    PurelyReflexive: 11,
    MAX: 11
  };
  var RingDrawingCodes = {};
  RingDrawingCodes[RingKind.PurelyReflexive] = "Cl";   // C = big circle, l = dot at left of circle
  RingDrawingCodes[RingKind.Irreflexive] = "Clb";      // b = bar across bottom of circle/ellipse
  RingDrawingCodes[RingKind.Symmetric] = "e";	      // e = ellipse with a dot each side
  RingDrawingCodes[RingKind.Asymmetric] = "eb";	      // 
  RingDrawingCodes[RingKind.AntiSymmetric] = "Eb";     // E = ellipse with dot on left, hollow dot on right
  RingDrawingCodes[RingKind.Intransitive] = "T3d";     // T = Big triangle with 3 dots, d = bar across bottom of triangle
  RingDrawingCodes[RingKind.Acyclic] = "C3b";	      // 
  RingDrawingCodes[RingKind.AcyclicIntransitive] = "CT3db";
  RingDrawingCodes[RingKind.AsymmetricIntransitive] = "etb"; // t = small triangle with dots and bar across bottom
  RingDrawingCodes[RingKind.SymmetricIntransitive] = "et";   // 
  RingDrawingCodes[RingKind.SymmetricIrreflexive] = "ec";	// c = small circle with bar

  paper.Constraint = function(x, y, roles) {
    var paper = this;
    var constraint = paper.ellipse(x, y, 15, 15)
	.attr({"stroke-width": 0, "stroke-dasharray": ".", fill: "#DDD"});
    constraint.icon = paper.path("M0 0")
      .attr({
	"stroke-width": 3,
	stroke: "#808",
	"stroke-linecap": "round",
	"stroke-linejoin": "round"
      });
    constraint.constraint = constraint;	      // For drop targeting
    constraint.icon.constraint = constraint;

    // For constraint connectors:
    constraint.x = function() { return constraint.attr("cx"); };
    constraint.y = function() { return constraint.attr("cy"); };

    /*--------------- Kind ---------------*/
    constraint.kind = ConstraintKind.Unknown;
    constraint.ring_kind = RingKind.AcyclicIntransitive;
    constraint.setKind = function(kind, ring_kind) {
      this.kind = kind;
      if (typeof ring_kind !== 'undefined') this.ring_kind = ring_kind;	// else no change
      // log("setting kind="+kind+", ring_kind="+this.ring_kind);

      var stroke_width = 3;
      var path;
      var select_hl_rx = 15;	      // X radius of highlight ellipse

      path = "M0 -11A11,11 0 1,1 0,11A11,11 0 1,1 0,-11";   // Start with a circle, commonly needed
      switch (this.kind) {
      case ConstraintKind.Uniqueness:
	path += "M-11 0L10 0"; break;
      case ConstraintKind.Preferred:
	path += "M-11 -2L10 -2M-11 3L10 3"; break;
      case ConstraintKind.Mandatory:
	path += "M0 -3a2.5 2.5 0 1,1 0,6a2.5 2.5 0 1,1 0,-6";
	path += "M0 -1a1 1 0 1,1 0,2a1 1 0 1,1 0,-1"; break;  // Center
      case ConstraintKind.Exclusion:
	path += "M7 7L-7 -7M7 -7L-7 7"; break;
      case ConstraintKind.ExOr:
	path += "M7 7L-7 -7M7 -7L-7 7"
	     + "M2 0L0 2L-2 0L0 -2L2 0"
	     + "M5 0L4 4L0 5L-4 4L-5 0L-4 -4L0 -5L4 -4L5 0"; break;
      case ConstraintKind.Equality:
	path += "M-6 -3L6 -3M-6 3L6 3"; break;
      case ConstraintKind.Subset:
	path += "M5 -5L-2 -5L-4 -4L-5 -3L-5 -1L-4 0L-2 1L5 1M5 5L-5 5"; break;
      case ConstraintKind.Frequency:	// REVISIT: This is not the same icon as ORM2
	path = "M-3 4L-3 -4M-3 -2L-1 -4L1 -4L3 -2L3 4"; break;
      case ConstraintKind.Ring:		// REVISIT: Draw ORM2 Deontic Ring types
	var codes = RingDrawingCodes[this.ring_kind];
	path = "M0 -11";
	for (var i = 0; i < codes.length; i++) {
	  switch (codes.charAt(i)) {
	  case 'C':	// Big circle
	    path += "A11,11 0 1,1 0,11A11,11 0 1,1 0,-11"; break;
	  case 'l':	// Dot at left of circle
	    path += "M-11 -3a2.5 2.5 0 1,1 0,6a2.5 2.5 0 1,1 0,-6"; break;
	  case 'b':	// Bar across bottom of circle/ellipse
	    path += "M0 15l0 -8"; break;
	  case 'e':	// Ellipse with dot at left and right
	    select_hl_rx = 20;
	    path += "M-15 -3a2.5 2.5 0 1,1 0,6a2.5 2.5 0 1,1 0,-6";	// Dot at left
	    path += "M-15 0A15,11 0 1,1 15,0A15,11 0 1,1 -15,0";	// Ellipse
	    path += "M15 -3a2.5 2.5 0 1,1 0,6a2.5 2.5 0 1,1 0,-6"; break; // Dot at right
	  case 'd':	// bar across bottom of big triangle
	    path += "M0 3l0 6"; break;
	  case 'E':	// Ellipse with dot at left, open circle at right
	    select_hl_rx = 20;
	    path += "M-15 -3a2.5 2.5 0 1,1 0,6a2.5 2.5 0 1,1 0,-6";  // Dot at left
	    path += "M-15 0A15,9 0 1,1 15,-3";		      // Lower half ellipse
	    path += "M15 -3a3 3 0 1,1 0,6a3 3 0 1,1 0,-6";    // Open dot at right
	    path += "M15 3A15,9 0 1,1 -15,0";		      // Lower half ellipse
	    break;
	  case 'T':	// Big triangle
	    path += "M0 -11L-9 6L9 6L0 -11"; break;
	  case '3':	// 3 dots at corners of big triangle
	    path += "M0,-14a2.5,2.5 0 1,1 0,6a2.5,2.5 0 1,1 0,-6"; // Big dot at top
	    path += "M-9,3.5a2.5,2.5 0 1,1 0,6a2.5,2.5 0 1,1 0,-6"; // Big dot at lower left
	    path += "M9,3.5a2.5,2.5 0 1,1 0,6a2.5,2.5 0 1,1 0,-6"; // Big dot at lower right
	    break;
	  case 't':	// Small triangle with bar across bottom
	    path += "M0 -7L-6 5L6 5L0 -7";
	    path += "M0 1l0 6"; break;
	  case 'c':	// Small circle with bar across bottom
	    path += "M0 -7A6,6 0 1,1 0,5A6,6 0 1,1 0,-7";
	    path += "M0 1l0 6"; break;
	  default:
	    log("Ring icon opcode not known: "+codes[i]);
	    break;
	  }
	}
	break;

      // case ConstraintKind.RoleValue:
      case ConstraintKind.Value:        // REVISIT: This is not the same icon as ORM2
	path += "M-5 -5L-6 -4L-6 -1L-7 0L-6 1L-6 4L-5 5"	// Left-brace
	     + "M-3 3L-3 3M0 3L0 3M3 3L3 3"		// Ellipsis
	     + "M5 -5L6 -4L6 -1L7 0L6 1L6 4L5 5";	// Right-brace
	stroke_width = 2;
	break;
      case ConstraintKind.Unknown:
      default:
	// A question mark
	path += "M-5 -4l1 -1l2 -1 l4 0 l2 1l1 1 l0 2l-1 1l-3 2l-1 1l0 2 m0 4l0 1";
	break;
      }
      this.setShape(path, select_hl_rx, stroke_width);
    };

    constraint.setShape = function(path, rx, stroke_width) {
      this.icon.attr({ path: path, "stroke-width": stroke_width });
      this.bindIconEvents();
      this.icon.translate(this.x()-1, this.y()-1);
      this.attr({rx: rx});
    };

    /*--------------- Connectors ---------------*/
    var connectors = [];
    constraint.connectors = connectors;
    constraint.connect = function(connector) {
      connectors.push(connector);
    };
    constraint.disconnect = function(connector) {
      array_delete(connectors, connector);
    };

    /*--------------- Role Sequences ---------------*/
    var role_sequences = [];
    constraint.role_sequences = role_sequences;
    constraint.addRoleSequence = function(roles) {
      var rs = [];
      role_sequences.push(rs);
      var i, cc = [];
      for (i in roles) {
	rs.push(roles[i]);
	cc.push(paper.ConstraintConnector(constraint, roles[i]));
      }
      for (i in cc) cc[i].redraw(); // Make sure that paired roles are drawn correctly
      // MESSAGE: Add role set to constraint
    };
    constraint.isRolePairMember = function(role) {
      // If this constraint has a pair of roles that are adjacent in the same fact type, they're a pair.
      var rs, fact_type;
      if (role_sequences.length == 2 && role_sequences[0].length == 1) {
	rs = [role_sequences[0][0], role_sequences[1][0]];
	fact_type = rs[0].fact_type;
	if ((rs[0] == role || rs[1] == role)
	 && fact_type == rs[1].fact_type
	 && fact_type.adjacentRoles(rs[0], rs[1]))
	  return rs;  // Handle case where two roles were added separately
	return null;
      }
      for (var i = 0; i < role_sequences.length; i++) {
	rs = role_sequences[i];
	fact_type = rs[0].fact_type;
	if (rs.length == 2
	 && (rs[0] == role || rs[1] == role)
	 && fact_type == rs[1].fact_type
	 && fact_type.adjacentRoles(rs[0], rs[1]))
	  return rs;
      }
      return null;
    };
    constraint.rolePairSlave = function(connector) {
      var rp = this.isRolePairMember(connector.role);
      if (!rp) return false;
      // log("rolePairSlave is "+(rp[0] == connector.role ? '0' : (rp[1] == connector.role ? '1' : 'neither')));
      return rp[1] == connector.role;
    };
    constraint.rolePairConnection = function(connector) {
      var rp = this.isRolePairMember(connector.role);
      if (!rp) return false;
      // log("rolePairConnection is "+(rp[0] == connector.role ? '0' : (rp[1] == connector.role ? '1' : 'neither')));
      if (rp[0] != connector.role) return null;
      // Return the point at the junction of the two roles, on the correct side.
      // REVISIT: The following doesn't work, because one connector jumps to the end.
      return rp[0].fact_type.roleJunction(rp, this.x(), this.y());
    };
    constraint.isFirstRole = function(role) {
      return role_sequences[0][0] == role;
    };

    /*--------------- Live and Die together ---------------*/
    constraint._real_remove = constraint.remove;
    constraint.remove = function() {
      // MESSAGE: Delete this constraint
      paper.revertFocus(this);
      while (connectors.length > 0)
        connectors[0].remove();
      this.icon.remove();
      this._real_remove();
    };
    constraint._real_translate = constraint.translate;
    constraint.translate = function(dx, dy) {
      this._real_translate(dx, dy);
      this.icon.translate(dx, dy);
      for (var c in connectors)
        connectors[c].redraw();
    };
    constraint._real_toFront = constraint.toFront;
    constraint.toFront = function() {
      for (var c in connectors)
        connectors[c].toFront();
      this._real_toFront();
      this.icon.toFront();
    };
    constraint._real_toBack = constraint.toBack;
    constraint.toBack = function() {
      this.icon.toBack();
      this._real_toBack();
      for (var c in connectors)
        connectors[c].toBack();
      background.toBack();
    };

    /*--------------- Object selection ---------------*/
    constraint.isSelected = function() {
      return paper.isSelected(this);
    };
    constraint.deselect = function() {
      this.attr({"stroke-width": 0});
      paper.deselect(this);
    };
    constraint.select = function(add) {
      if (this.isSelected()) { return; }
      if (!add) { paper.deselectAll(); }
      this.attr({"stroke-width": 1});
      paper.select(this);
    };

    /*--------------- Object highlighting ---------------*/
    constraint.highlight = function() {
      this.attr({fill: "#DDD"});
      paper.showHelp("Double-click or type one of 'upmfxo=rsv' to change constraint type"
	+ (this.kind != ConstraintKind.Ring ? "" : ", R to change ring type")
      );
      paper.takeFocus(this);
    };
    constraint.unhighlight = function() {
      this.attr({fill: "#FFF"});
      paper.revertFocus();
      paper.showHelp("");
    };
    constraint.activate = function() {
      constraint.setKind(constraint.kind+1 == ConstraintKind.MAX ? 1 : constraint.kind+1);
      // MESSAGE: Change constraint kind
    };

    /*--------------- Key handling ---------------*/
    constraint.handleKey = function(k, e) {
      switch (k.str) {
      case 'u': this.setKind(ConstraintKind.Uniqueness); break;
      case 'p': this.setKind(ConstraintKind.Preferred); break;
      case 'm': this.setKind(ConstraintKind.Mandatory); break;
      case 'f': this.setKind(ConstraintKind.Frequency); break;
      case 'x': this.setKind(ConstraintKind.Exclusion); break;
      case 'o': this.setKind(ConstraintKind.ExOr); break;
      case '=': this.setKind(ConstraintKind.Equality); break;
      case 'r': this.setKind(ConstraintKind.Ring); break;
      case 'R':
	var next_ring_kind = this.ring_kind+1 <= RingKind.MAX ? this.ring_kind+1 : 1;
	this.setKind(ConstraintKind.Ring, next_ring_kind); break;
      case 's': this.setKind(ConstraintKind.Subset); break;
      case 'v': this.setKind(ConstraintKind.Value); break;
      // case 'v': this.setKind(ConstraintKind.RoleValue); break;
      default:
	return paper.handleKey(k, e);
      }
      // MESSAGE: Change constraint kind
      return false;
    };

    var dragged_from_x, dragged_from_y;
    constraint.dragStart = function(x, y, mousedownevent, mousemoveevent) {
      dragged_from_x = this.x();
      dragged_from_y = this.y();
      return this;
    };
    constraint.dragFinish = function(dropped_on, x, y, event) {
      // MESSAGE: Move constraint from dragged_from_x, dragged_from_y to this.x(), this.y()
    };

    /*--------------- Event Handlers ---------------*/
    constraint.bindIconEvents = function() {
      // Called from setShape from setKind (below)
      constraint.click(function(event) { constraint.select(event.shiftKey); return false; });
      constraint.icon.hover(function() { constraint.highlight(); }, function() { constraint.unhighlight(); });
      constraint.dblclick(function(event) { constraint.activate(); });
      constraint.icon.draggable({drag_obj: constraint, right_button: false});
    };

    constraint.bindEvents = function() {
      constraint.draggable({right_button: false});
      constraint.click(function(event) { constraint.select(event.shiftKey); return false; });
      constraint.hover(function() { constraint.highlight(); }, function() { constraint.unhighlight(); });
      constraint.dblclick(function(event) { constraint.activate(); });
    };
    constraint.bindEvents();

    constraint.setKind(ConstraintKind.Unknown);	// Binds icon events
    constraint.addRoleSequence(roles);

    return constraint;
  };

  // Create a new object when the background is double-clicked:
  var doubled = function(event) {
    event = jQuery.event.fix(event);
    if ((event.target || event.srcElement) != background.node) { // Ignore an event that propagated
      return false;
    }
    var position = paper.pageOffset();
    var currentScale = $(paper.canvas).attr('currentScale') || 1;
    var n = paper.ObjectType("New", event.pageX/currentScale-position.left, event.pageY/currentScale-position.top);
    n.text.selectAll();
    // MESSAGE: New object_type
    return false;
  };
  background.dblclick(doubled);

  /* Keyboard dispatcher */
  paper.keyboardFocus = paper;

  paper.handleKey = function(k, e) {
    if (k.code == Key.BACKSPACE || k.code == Key.DELETE) {
      // REVISIT: MESSAGE: Bundle deletion of selected_objects, if > 1
      while (selected_objects.length > 0) {
        var to_delete = selected_objects[0];
        to_delete.deselect();
        to_delete.remove();
      }
      return false;   // Don't propagate the DELETE key - it make some browsers go Back
    } else {
      // alert("Unhandled key "+k.code+" str '"+k.str+"'");
      return true;  // Allow event to propagate
    }
  };

  paper.takeFocus = function(obj) {
    paper.keyboardFocus = obj;
  };
  paper.hasFocus = function(obj) {
    paper.keyboardFocus === obj;
  };
  paper.revertFocus = function(obj) {
    if (!obj || obj === paper.keyboardFocus)
      paper.keyboardFocus = paper;
  };

  /*
   * An object that wants keyboard focus should call paper.takeFocus passing an
   * object that provides a handleKey method, and calling revertFocus when done.
   * At some point, a focus stack might be needed.
   *
   * A handleKey method gets passed a key object (as well as the event) which is either:
   * { code : the special key code } OR
   * { str : the string representing a printable character }
   *
   * handleKey should return true to allow the event to bubble, false to stop it.
   */
  /* Record the key code from a keydown event to use on auto-repeat and on keyup if no keypress follows */
  var downKey;

  /* Handle special keys using keydown; these browsers don't send keypress for these events */
  if ($.browser.msie || $.browser.safari) {
    // Handle special keys using keydown; these browsers don't send keypress for these events
    $(document).bind('keydown', function(e){
      if (downKey && downKey == e.keyCode)    // Auto-repeat
	paper.keyboardFocus.handleKey({code: downKey}, e);
      downKey = e.keyCode;
      if ((e.charCode || e.which) == 8) return false;	/* Don't go to the previous page */
      return true;
    });
    $(document).bind('keyup', function(e){
      if (downKey)  // There was no keypress in between; must be a special key
	paper.keyboardFocus.handleKey({code: downKey}, e);
      downKey = null;
      return true;
    });
  }

  if (false && jQuery.browser.safari) {
    // REVISIT: Need to handle non-text keys below, and cut/copy/paste; but not double-handle text below
    $(document).live('textInput', function(e) {
      return paper.keyboardFocus.handleKey({str: e.originalEvent.data}, e);
    });
  } else {
    $(document).bind('keypress',function(e) {
      var charCode = e.charCode || e.which;
      downKey = null;
      if ($.browser.opera) {
	if (!e.originalEvent.which)   // Without this, left-arrow and single-quote are identical
	  charCode = null;
	e.ctrlKey = e.metaKey;   // Opera also sets metaKey instead of ctrlKey :-(
      } else if ($.browser.msie)
	charCode = e.which;
      else
	charCode = e.charCode;
      if (charCode && !e.metaKey && !e.altKey && !e.ctrlKey && charCode >= 32)
	return paper.keyboardFocus.handleKey({str: String.fromCharCode(charCode)}, e);
      else  // control key on MSIE, or any special key on Firefox, etc
	return paper.keyboardFocus.handleKey({code: e.keyCode}, e);
    });
  }

  // Remove selection from an object when the background is clicked:
  background.click(function(event) {
    event = jQuery.event.fix(event);
    if (event.target != background.node)
      return false;
    if (!event.shiftKey)
      paper.deselectAll();
    return false;
  });

  paper.showHelp("Double-click to create an object type");
}

/*
 * Geometry from http://local.wasp.uwa.edu.au/~pbourke/geometry
 *
 * Calculate the intersection of a ray and a circle
 * The line segment is defined from p1 to p2
 * The circle is of radius r and centered at sc
 * There are potentially two points of intersection given by
 * p = p1 + mu1 (p2 - p1)
 * p = p1 + mu2 (p2 - p1)
 * Return FALSE if the ray doesn't intersect the sphere.
 */
function LineCircle(p1x, p1y, p2x, p2y, scx, scy, r)
{
  var dp = { x: p2x - p1x, y: p2y - p1y };
  var a = dp.x * dp.x + dp.y * dp.y;
  var b = 2 * (dp.x * (p1x - scx) + dp.y * (p1y - scy));
  var c = scx * scx + scy * scy;
  c += p1x * p1x + p1y * p1y;
  c -= 2 * (scx * p1x + scy * p1y);
  c -= r * r;
  var bb4ac = b * b - 4 * a * c;
  if ((a > -0.000001 && a < 0.000001) || bb4ac < 0)
    return null;

  return [
    (-b + Math.sqrt(bb4ac)) / (2 * a),
    (-b - Math.sqrt(bb4ac)) / (2 * a)
  ];
}

// Intersection between two line segments p1-p2, p3-p4
function LineLine(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y)
{
  var ua_num = 1.0*(p4x-p3x)*(p1y-p3y) - 1.0*(p1x-p3x)*(p4y-p3y);
  var den = 1.0*(p2x-p1x)*(p4y-p3y) - 1.0*(p4x-p3x)*(p2y-p1y);
  if (den < 0.00001 && den > -0.00001) return null; // Parallel lines
  var ua = ua_num/den;	// Distance ratio along segment p1-p2
  if (ua < -0.00001 || ua > 1.00001) return null; // Outside segment p1-p2
  var ub_num = 1.0*(p2x-p1x)*(p1y-p3y) - 1.0*(p1x-p3x)*(p2y-p1y);
  var ub = ub_num/den;
  if (ub < -0.00001 || ub > 1.00001) return null; // Outside segment p3-p4
  return [ p1x + ua*(p2x-p1x), p1y + ua*(p2y-p1y) ];
}

// Find where the line ex,ey-x,y intersects the rect which has corner radius c
// This function written by Clifford Heath.
function IntersectRoundRect(ex, ey, x, y, rect, c) {
  var x2 = rect.x+rect.width, y2 = rect.y+rect.height;
  var x1c = rect.x+c, x2c = rect.x+rect.width-c;	  // Find ends of line segs (also arc centres)
  var y1c = rect.y+c, y2c = rect.y+rect.height-c;
  var p =
    LineLine(ex,ey,x,y,x1c,rect.y,x2c,rect.y) ||    // Top segment
    LineLine(ex,ey,x,y,rect.x,y1c,rect.x,y2c) ||    // Left segment
    LineLine(ex,ey,x,y,x1c,y2,x2c,y2) ||	    // Bottom segment
    LineLine(ex,ey,x,y,x2,y1c,x2,y2c);		    // Right segment
  if (p) return { x: p[0], y: p[1] };

  var r;
  if (ex < x) {
    if (ey < y)
      r = LineCircle(ex, ey, x, y, x1c, y1c, c);  // Top left
    else
      r = LineCircle(ex, ey, x, y, x1c, y2c, c);  // Bottom left
  } else {
    if (ey < y)
      r = LineCircle(ex, ey, x, y, x2c, y1c, c);  // Top right
    else
      r = LineCircle(ex, ey, x, y, x2c, y2c, c);  // Bottom right
  }
  if (r) return { x: ex+(x-ex)*r[1], y: ey+(y-ey)*r[1]};

  return { x: x, y: y };  // Shouldn't happen
}
